package by.brest.bspc.repository;

import by.brest.bspc.domain.Grade;
import by.brest.bspc.domain.Student;
import by.brest.bspc.repository.converters.StudentReadConverter;
import com.mongodb.AggregationOutput;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static by.brest.bspc.repository.Constants.ACTIVE_STATUS;
import static by.brest.bspc.repository.Constants.ARCHIVE_STATUS;
import static by.brest.bspc.repository.Constants.EXPELLED_STATUS;
import static by.brest.bspc.repository.Constants.CLASS_NUMBER;
import static by.brest.bspc.repository.Constants.DISCIPLINE_NAME;
import static by.brest.bspc.repository.Constants.GRADE_LIST;
import static by.brest.bspc.repository.Constants.GRADE_TYPE;
import static by.brest.bspc.repository.Constants.GRADUATED_CLASS;
import static by.brest.bspc.repository.Constants.GROUP_NAME;
import static by.brest.bspc.repository.Constants.ID;
import static by.brest.bspc.repository.Constants.SEMESTER_NUMBER;
import static by.brest.bspc.repository.Constants.STATUS;
import static by.brest.bspc.repository.Constants.STUDENT_NAME;
import static org.springframework.data.mongodb.core.query.Criteria.where;

/**
 * This class contains methods for the processing of the data of student that stored into mongoDB database.
 *
 * @date: 06/09/13 17:00
 * @author: Natallia
 */
@Repository
public class StudentRepositoryImpl implements StudentRepository {
    private static final Logger LOG = LoggerFactory.getLogger(StudentRepositoryImpl.class);

    @Resource
    private MongoOperations mongoOperations;

    private String collectionName;
    private Query query = new Query();
    private Update update = new Update();

    /**
     * Sets collection name.
     *
     * @param collectionName is a collection name.
     */
    @Required
    public void setCollectionName(String collectionName) {
        LOG.debug(">> Collection name: " + collectionName);

        this.collectionName = collectionName;
    }

    /**
     * Sets <code>mongoOperations</code>.
     * For test only.
     *
     * @param mongoOperations is a <code>mongoOperations</code>.
     */
    public void setMongoOperations(MongoOperations mongoOperations) {
        LOG.debug(">> mongoOperations: " + mongoOperations);

        this.mongoOperations = mongoOperations;
    }

    /**
     * Sets query.
     * For test only.
     *
     * @param query is a query
     */
    public void setQuery(Query query) {
        LOG.debug(">> Query: " + query);

        this.query = query;
    }

    /**
     * Sets update.
     * For test only.
     *
     * @param update is a query
     */
    public void setQuery(Update update) {
        LOG.debug(">> Update: " + update);

        this.update = update;
    }

    /**
     * {@inheritDoc}
     *
     * @param student is a student that need to save into mongoDB database.
     */
    @Override
    public void saveStudent(Student student) {
        LOG.debug(">> Student object that will be saved: " + student);

        mongoOperations.save(student, collectionName);
    }

    /**
     * {@inheritDoc}
     *
     * @return list of students.
     */
    @Override
    public List<String> updateGraduatedStudents() {
        DBObject matchStatus = new BasicDBObject("$match", new BasicDBObject(STATUS, ACTIVE_STATUS));
        DBObject matchClassNumber = new BasicDBObject("$match", new BasicDBObject(CLASS_NUMBER, GRADUATED_CLASS));
        DBObject group = new BasicDBObject("$group", new BasicDBObject(ID, "$" + GROUP_NAME));

        AggregationOutput aggregate = mongoOperations.getCollection(collectionName).aggregate(matchStatus, matchClassNumber, group);

        Iterable<DBObject> results = aggregate.results();
        Iterator<DBObject> iterator = results.iterator();
        List<String> graduatedGroups = new ArrayList<>();
        while (iterator.hasNext()) {
            DBObject graduatedGroup = iterator.next();
            graduatedGroups.add((String) graduatedGroup.get(ID));
        }

        LOG.debug(">> Graduated groups: " + graduatedGroups);

        query.addCriteria(where(GROUP_NAME).in(graduatedGroups));
        update.set(GROUP_NAME, ARCHIVE_STATUS);

        mongoOperations.updateMulti(query, update, collectionName);

        return graduatedGroups;
    }

    /**
     * {@inheritDoc}
     *
     * @param groupName name of the group.
     * @return current class number.
     */
    @Override
    public int getCurrentClassByGroupName(String groupName) {
        DBObject match = new BasicDBObject("$match", new BasicDBObject(GROUP_NAME, groupName));
        DBObject group = new BasicDBObject("$group", new BasicDBObject(ID, "$" + CLASS_NUMBER));
        DBObject sort = new BasicDBObject("$sort", new BasicDBObject(ID, -1));
        DBObject limit = new BasicDBObject("$limit", 1);

        AggregationOutput aggregate = mongoOperations.getCollection(collectionName).aggregate(match, group, sort, limit);

        Iterable<DBObject> results = aggregate.results();
        DBObject result = results.iterator().next();

        return (int) result.get(ID);
    }

    /**
     * {@inheritDoc}
     *
     * @param groupName      group name.
     * @param semesterNumber semester number.
     * @return list of students.
     */

    @Override
    public List<Student> getStudentsList(String groupName, int semesterNumber) {
        LOG.debug(">> Start getStudentsList method with parameters:");
        LOG.debug(">> groupName: " + groupName);
        LOG.debug(">> semesterNumber: " + semesterNumber);

        DBObject matchFirstParams = new BasicDBObject();
        matchFirstParams.put(GROUP_NAME, groupName);
        matchFirstParams.put(STATUS, ACTIVE_STATUS);
        DBObject matchFirst = new BasicDBObject("$match", matchFirstParams);

        DBObject unwind = new BasicDBObject("$unwind", "$" + GRADE_LIST);
        DBObject matchSecond = new BasicDBObject("$match", new BasicDBObject(GRADE_LIST + "." + SEMESTER_NUMBER, semesterNumber));

        DBObject sortFirstParams = new BasicDBObject();
        sortFirstParams.put(GRADE_LIST + "." + GRADE_TYPE, -1);
        sortFirstParams.put(GRADE_LIST + "." + DISCIPLINE_NAME, 1);
        DBObject sortFirst = new BasicDBObject("$sort", sortFirstParams);

        DBObject groupFirstParams = new BasicDBObject();
        groupFirstParams.put(ID, "$" + ID);
        groupFirstParams.put(STUDENT_NAME, new BasicDBObject("$last", "$" + STUDENT_NAME));
        groupFirstParams.put(GRADE_LIST, new BasicDBObject("$push", "$" + GRADE_LIST));
        DBObject groupFirst = new BasicDBObject("$group", groupFirstParams);

        DBObject sortSecond = new BasicDBObject("$sort", new BasicDBObject(STUDENT_NAME, 1));

        LOG.debug(">> Created query: ");
        LOG.debug(">> " + matchFirst);
        LOG.debug(">> " + unwind);
        LOG.debug(">> " + matchSecond);
        LOG.debug(">> " + sortFirst);
        LOG.debug(">> " + groupFirst);
        LOG.debug(">> " + sortSecond);

        AggregationOutput aggregate = mongoOperations.getCollection(collectionName).aggregate(matchFirst, unwind, matchSecond, sortFirst, groupFirst, sortSecond);

        Iterable<DBObject> resultList = aggregate.results();

        LOG.debug(">> Result list: " + resultList);

        Iterator<DBObject> list = resultList.iterator();
        List<Student> studentsList = new ArrayList<>();
        StudentReadConverter converter = new StudentReadConverter();

        while (list.hasNext()) {
            studentsList.add(converter.convert(list.next()));
        }

        LOG.debug(">> List of students: " + studentsList);

        return studentsList;
    }

    /**
     * {@inheritDoc}
     *
     * @param studentsList list of students.
     */
    @Override
    public void updateStudentsList(List<Student> studentsList) {
        LOG.debug(">> Start updateStudentsList method with parameters:");
        LOG.debug(">> studentsList: " + studentsList);

        if (!studentsList.isEmpty()) {
            for (Student student : studentsList) {
                Query firstQuery = new Query();
                firstQuery.addCriteria(where(ID).is(student.getId()));
                Student studentToUpdate = mongoOperations.findOne(firstQuery, Student.class, collectionName);

                LOG.debug(">> Student from base for update: " + studentToUpdate);

                if (studentToUpdate != null) {

                    for (Grade grade : student.getGradeList()) {
                        for (Grade gradeToUpdate : studentToUpdate.getGradeList()) {
                            if (gradeToUpdate.getDisciplineName().equals(grade.getDisciplineName())
                                    && (gradeToUpdate.getSemesterNumber() == grade.getSemesterNumber())) {

                                gradeToUpdate.setMark(grade.getMark());
                                gradeToUpdate.setRespect(grade.getRespect());
                                gradeToUpdate.setDirespect(grade.getDirespect());
                            }

                        }
                    }

                    LOG.debug(">> Updated student: " + studentToUpdate);

                    saveStudent(studentToUpdate);

                    if (student.getStatus().equals(EXPELLED_STATUS)) {

                        Query secondQuery = new Query();

                        secondQuery.addCriteria(where(STUDENT_NAME).is(studentToUpdate.getName())
                                .andOperator(Criteria.where(GROUP_NAME).is(studentToUpdate.getGroupName())));
                        update.set(STATUS, EXPELLED_STATUS);

                        mongoOperations.updateMulti(secondQuery, update, collectionName);

                        LOG.debug(">> Student was expelled.");
                    }
                }
            }
        }

    }
}
